Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visuals, Position #12

Merged
merged 10 commits into from
May 28, 2024
Merged

Conversation

wtfrank
Copy link
Contributor

@wtfrank wtfrank commented Apr 2, 2024

There are two parts to this pull request.

Visuals

Firstly we have an implementation of visuals for arena, which allows you to do horrific things like this.
image
The visual objects and the means of styling them are largely the same in arena as world, so I've been able to bring over the style structs from the rust world api, such as TextStyle, PolyStyle and CircleStyle. However the underlying API is a bit different so we can't serialise the visuals on the rust side then send them over to javascript in a single call (as the rust world api does); instead we have to keep switching between rust and js as we build up the objects.

A challenge with the design of this interface has been how to handle the specification of positions of the visuals. They're typically described in the game documentation as May be GameObject or any object containing x and y properties. Duck typing is not available in the rust type system, so I've considered two ways to pass these parameters.

  • We could just accept &JsValue everywhere (for example pub fn circle(this: &Visual, pos: &JsValue, style: &JsValue) and rely on the caller to do a serde conversion from whatever struct they want. This is close to the javascript semantics, but it essentially eliminates type checking, and why would you be writing rust code if you didn't like your code to be type-checked?
  • What I've done instead is to define struct VisualPosition {x: f32, y:f32} alongside helper traits to obtain a VisualPosition from a GameObject. This means the api method can be type-checked: pub fn circle(self: &Visual, pos: &VisualPosition, style: Option<&CircleStyle>).
impl<T> From<T> for VisualPosition
where
    T: GameObjectProperties,
{
    fn from(obj: T) -> Self {
        VisualPosition {
            x: obj.x() as f32,
            y: obj.y() as f32,
        }
    }
}

This lets us do VisualPosition::from(&creep). But sometimes we're not dealing with GameObjects - perhaps a desired future attack position, or perhaps we're interested in the path a creep will be taking. So I've also added VisualPosition::from() for Position. This leads onto part 2 of this change, which is an improved Position struct from pathfinder. I've added a pos() method to GameObjects, which returns a Position.

  Visual::new(None, false)
      .line(
        &VisualPosition::from(creep.pos()).offset(-1.5, -1.5),
        &VisualPosition::from(&creep).offset(1.5, 1.5),
        None,
      )
      .rect(&VisualPosition::from(creep.pos()), 1.0, 1.0, None);

Position

The second part of this change lets us obtain pathfinder's Position struct from every GameObject via a pos() method, and the Position struct is augmented with PartialEq and Hash traits that allow use to do useful things with Position, and I've added a From<Position> for JsValue so that it can be easily converted into JsValue for bindgen api functions.

I've added Add for Position trait, as well as checked_add_direction() and saturating_add_direction(), to keep commonality with the world api.

Note that Position contains u8 x/y coordinates that correspond to coordinates in the game world, while VisualPosition contains floating point x/y coordinates as visuals can be placed at fractional coordinate locations.

This Position work is partly here because it works nicely with the visual api methods, and partly because being able to use Position throughout your bot code is a useful alternative to x/y.

I don't think this Position work is too opinionated: Ranamar on discord has done some similar changes to his copy of the API so other people probably have too, adding pos() makes better use of an existing structure (rather than inventing something new), and it's a usage pattern familiar from world and the world API. So I don't think the Position commit is a big deal, but if it's seen as undesirable it could be separated from the Visual commit (I've kept the two commits largely independent).

Demo Code

I've created a function to test the API, and it generated the visuals in the image above. I'll include it here as I think it's useful for showing how the new methods can be used.

fn test_visuals() {
  Visual::new(None, false).circle(
    &VisualPosition::from(Position { x: 25, y: 25 }),
    Some(&CircleStyle::default().radius(10.0).opacity(0.6)),
  );
  Visual::new(Some(2), false).circle(
    &VisualPosition::from(Position { x: 22, y: 25 }),
    Some(&CircleStyle::default().radius(5.0).opacity(1.0).fill("#00ff00")),
  );
  Visual::new(Some(-2), false).circle(
    &VisualPosition::from(Position { x: 19, y: 25 }),
    Some(&CircleStyle::default().radius(5.0).opacity(1.0).fill("#ff0000")),
  );

  let creep = game::utils::get_objects_by_prototype(prototypes::CREEP)
    .into_iter()
    .find(|c| c.my());
  if let Some(creep) = creep {
    Visual::new(None, false)
      .line(
        &VisualPosition::from(creep.pos()).offset(-1.5, -1.5),
        &VisualPosition::from(&creep).offset(1.5, 1.5),
        None,
      )
      .rect(&VisualPosition::from(creep.pos()), 1.0, 1.0, None);
  }

  Visual::new(None, false).poly(
    &vec![
      VisualPosition { x: 10.0, y: 10.0 },
      VisualPosition { x: 15.0, y: 10.0 },
      VisualPosition { x: 15.0, y: 15.0 },
      VisualPosition { x: 10.0, y: 10.0 },
    ],
    Some(&PolyStyle::default().stroke_width(0.7).line_style(LineDrawStyle::Dotted)),
  );

  Visual::new(None, false).text(
    "screeps",
    &VisualPosition { x: 0.0, y: 20.0 },
    Some(
      &TextStyle::default()
        .color("#0000FF")
        .font_style("bold italic 5.5 Times New Roman")
        .stroke("#00ff00")
        .stroke_width(0.4)
        .background_color("#ff0000")
        .opacity(0.6),
    ),
  );

  Visual::new(None, false).text(
    "arena",
    &VisualPosition { x: 5.0, y: 25.0 },
    Some(&TextStyle::default().color("#0000FF").font_size(5.5)),
  );

  let line_start = VisualPosition { x: 0.0, y: 22.5 };
  Visual::new(None, false)
    .line(&line_start, &line_start.offset(15.0, 0.0), Some(&LineStyle::default()))
    .rect(
      &VisualPosition { x: -1.0, y: 18.0 },
      6.0,
      9.0,
      Some(&RectStyle::default().opacity(0.2).fill("#00ffff")),
    );
}

edit: updated to remove the stuff about AsRef, that wasn't needed for VisualPosition::from() of GameObject derived types.

add equality/hash traits to Position so that it can be used as keys in
collections such as HashSet

give every GameObject a pos() method which returns Position
@wtfrank wtfrank force-pushed the visuals_and_position branch from 62d92a5 to 9365ddd Compare April 2, 2024 22:19
reuses what it can from world rust api (the style structures), however the arena visual API doesn't expose the serialisation methods that world has, so arena visuals work differently and probably involve more js<->wasm switches.
@wtfrank wtfrank force-pushed the visuals_and_position branch from 9365ddd to c2d084c Compare April 3, 2024 17:31
@wtfrank wtfrank changed the title DRAFT: Visuals, Position Visuals, Position Apr 28, 2024
wtfrank and others added 6 commits May 2, 2024 22:08
similar methods to arena api: checked_add_direction(),
saturating_add_direction() and Add<Direction> which will panic if bounds
exceeded.

The bounds check is based on new constants ROOM_WIDTH/ROOM_HEIGHT.
@shanemadden shanemadden merged commit 6ad8ca2 into rustyscreeps:main May 28, 2024
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants